package engine.core.scene;

import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glGetInteger;
import static org.lwjgl.opengl.GL13.*;

import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;

import engine.camera.Camera;
import engine.camera.DefaultCamera;
import engine.core.OpenGLView;
import engine.gameObject.GameObject;
import engine.gui.GUIStyle;
import engine.gui.WindowManager;
import engine.light.Light;
import engine.light.LightManager;
import engine.shader.DefaultShader;
import engine.shader.Shader;
import engine.util.draw.Draw2D;
import engine.util.error.Check;
import engine.util.fbo.FBORenderer;
import engine.util.math.Matrix;
import engine.util.timer.Timer;

public abstract class Scene {

	//The current camera. 
	private Camera currentCamera;
	
	//The core. 
	private OpenGLView openGLView;
	
	//A window manager for handling the hud. 
	private WindowManager windowManager;
	
	//The light manager to handle the limited light sources of opengl
	private LightManager lightManager;
	
	//The name of the scene. This name should be unique for correct adressing. 
	private String name;
	
	//A sky object, that will always be behind the scene. 
	private GameObject skyObject;
	
	//Sphere trees for near and far pane
	private SphereTree nearSphereTree;
	private SphereTree farSphereTree;
	
	//FBO's for rendering the different planes. 
	private FBORenderer nearFBO;
	private FBORenderer farFBO;
	private FBORenderer skyFBO;
	
	//Address to the texture to screen shader. 
	private Shader texToScreenShader;
	private Shader skyFBOShader;
	private Shader defaultShader;
	
	//The two frustums. 
	private static Frustum nearFrustum;
	private static Frustum farFrustum;
	
	//stack for projection matrix
	private Matrix projectionMatrix;
	
	//numbers of rendered polygons
	private int numPolygons;
	private int numDrawCalls;
	private float renderLoopTime;
	
	//a list of objects, which are going to be checked for collision detection
	private ArrayList<GameObject> rigidbodies;
		
	public Scene(String name) {
		this.name = name;
		
		currentCamera = new DefaultCamera();
		
		nearFrustum = new Frustum();
		farFrustum = new Frustum();
		
		nearSphereTree = new SphereTree(this);
		farSphereTree = new SphereTree(this);
		
		projectionMatrix = new Matrix();
		
		rigidbodies = new ArrayList<GameObject>();
	}
	
	public void addGameObject(GameObject gameObject) {
		//insert objects to trees
		if(gameObject.isInNearPane()) {
			nearSphereTree.addGameObject(gameObject);
		} else {
			farSphereTree.addGameObject(gameObject);
		}
		
		if(gameObject.isRigidbody()) {
			rigidbodies.add(gameObject);
		}
	}
	
	public void removeGameObject(GameObject gameObject) {
		nearSphereTree.removeGameObject(gameObject);
		farSphereTree.removeGameObject(gameObject);
	}
	
// TODO: clean up and add the possiblity, that objects can be set to rigidbodies after adding
//	public void addCheckCollider(GameObject gameObject) {
//		rigidbodies.add(gameObject);
//	}
//	
//	public void removeCheckCollider(GameObject gameObject) {
//		rigidbodies.remove(gameObject);
//	}
	
	public void addLight(Light light) {
		lightManager.add(light);
	}
	
	public void removeLight(Light light) {
		lightManager.remove(light);
	}
	
	public void initAbs() {
		lightManager = new LightManager(this);
		
		Check.error("light manager (" + name + "): ");
		
		IntBuffer buffer = BufferUtils.createIntBuffer(16);
		GL11.glGetInteger(GL11.GL_VIEWPORT, buffer);
		windowManager = new WindowManager(buffer.get(2), buffer.get(3));
		windowManager.setScene(this);
		buffer.clear();
		
		Check.error("Window manager (" + name + "): ");
		
		init();
		
		Check.error("Subclass init (" + name + "): ");
		
		//init sphere tree
		nearSphereTree.init();
		farSphereTree.init();
		
		Check.error("Spheretree init (" + name + "): ");
		
		//Setup FBOs with maximum image size
		//get the maximum size of textures
		DisplayMode displayMode = Display.getDesktopDisplayMode();
		int texWidth = 1;
		while(texWidth < displayMode.getWidth()) {
			texWidth *= 2;
		}
		int texHeight = 1;
		while(texHeight < displayMode.getHeight()) {
			texHeight *= 2;
		}
		
		texWidth = Math.min(texWidth, glGetInteger(GL_MAX_TEXTURE_SIZE));
		texHeight = Math.min(texHeight, glGetInteger(GL_MAX_TEXTURE_SIZE));
		
		//create fbos. 
		nearFBO = FBORenderer.getRenderer("near", texWidth, texHeight, false);
		farFBO = FBORenderer.getRenderer("far", texWidth, texHeight, false);
		skyFBO = FBORenderer.getRenderer("sky", texWidth, texHeight, false);
		
		Check.error("FBO init (" + name + "): ");
		
		//load shader for fbos
		Map<Integer, String> map = new HashMap<Integer, String>();
		map.put(0, "vertex_in");
		map.put(1, "texcoord_in");
		
		texToScreenShader = new Shader("shader/engine/core/texToScreen.shader", map);
		texToScreenShader.enable();
		texToScreenShader.sendUniform1i("tex", 0);
		texToScreenShader.sendUniform1i("depthTexture", 1);
		texToScreenShader.sendUniform1f("maxDepth", 1);
		texToScreenShader.disable();
		
		defaultShader = new DefaultShader();
		
		map = new HashMap<Integer, String>();
		map.put(0, "vertex_in");
		map.put(1, "texcoord_in");
		map.put(2, "normal_in");
		skyFBOShader = new Shader("shader/engine/default/textureOnly.shader", map); 
		
		skyFBOShader.enable();
		skyFBOShader.sendUniform1i("tex", 0);
		skyFBOShader.disable();
		
		Check.error("Shader init (" + name + "): ");
	}
	
	public void init() {}

	public void updateView(int width, int height) {
		windowManager.updateView(width, height);

		currentCamera.setWidth(width);
		currentCamera.setHeight(height);
	}
	
	public void setCurrentAbs() {
		nearFrustum.setScene(this);
		farFrustum.setScene(this);
		
		setCurrent();
	}
	
	public void setCurrent() {}
	
	public void unsetCurrent() {}
	
	public void renderAbs(float timeElapsed) {
		float time = Timer.getTime();
		//reset statistics
		numDrawCalls = 0;
		numPolygons = 0;
		
		nearSphereTree.update(timeElapsed);
		farSphereTree.update(timeElapsed);
		
		//collision detection
		detectCollisions();
		
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		
		//update camera and extract frustum
		currentCamera.update(timeElapsed);
		currentCamera.look();
		
		//render sky into fbo and render texture
		renderSkyObject(timeElapsed);
		
		defaultShader.enable();
		defaultShader.sendUniformMatrix4("viewMatrix", getCurrentCamera().getViewMatrix().toFloatBuffer());
		getLightManager().apply(defaultShader);
		//render farpane into fbo and render texture
		renderFarPane();
		
		//render nearpane into fbo and render texture
		renderNearPane();
		
		defaultShader.disable();
		
		//renderes the prepared fbos to the screen
		renderTexturesToScreen();
		
		render(timeElapsed);
		
		windowManager.render();
		
		renderLoopTime = Timer.getTime() - time;
	}
	
	private void detectCollisions() {
		for(GameObject gameObject: rigidbodies) {
			detectCollision(gameObject);
		}
	}
	
	private boolean detectCollision(GameObject g) {
		boolean nearCollision = nearSphereTree.checkCollision(g);
		boolean farCollision = farSphereTree.checkCollision(g);
		if(!nearCollision && ! farCollision) {
			for(GameObject child: g.getChildren()) {
				if(child.isRigidbody() && detectCollision(child)) {
					return true;
				}
			}
			
			return false;
		}
		
		return nearCollision && farCollision;
	}
	
	private void renderNearPane() {
		nearFBO.preRender();
		
		currentCamera.prepareNearPane();
		
		nearFrustum.update();
		
		nearSphereTree.render(nearFrustum, defaultShader);
		
		nearFBO.postRender();
	}
	
	private void renderFarPane() {
		farFBO.preRender();
		
		currentCamera.prepareFarPane();
		
		farFrustum.update();
		
		farSphereTree.render(farFrustum, defaultShader);
		
		farFBO.postRender();
	}
	
	private void renderSkyObject(float timeElapsed) {
		skyFBOShader.enable();
		skyFBOShader.sendUniformMatrix4("viewMatrix", getCurrentCamera().getViewMatrix().toFloatBuffer());
		skyFBO.preRender();
		
		if(skyObject != null) {
			currentCamera.prepareFarPane();
			
			skyObject.updateAbs(timeElapsed);
			skyObject.setPosition(currentCamera.getEyeX(), currentCamera.getEyeY(), currentCamera.getEyeZ());
			skyObject.renderAbs(null, skyFBOShader);
		}
		
		skyFBO.postRender();
		skyFBOShader.disable();
	}
	
	private void renderTexturesToScreen() {
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		
		projectionMatrix.setIdentity();
		projectionMatrix.ortho(0, windowManager.getWidth(), 0, windowManager.getHeight(), 0, 1);
				
		skyFBOShader.enable();
		skyFBOShader.sendUniformMatrix4("viewMatrix", Matrix.identity());
		skyFBOShader.sendUniformMatrix4("modelMatrix", Matrix.identity());
		skyFBOShader.sendUniformMatrix4("projectionMatrix", projectionMatrix.toFloatBuffer());
		
		//draw sky fbo
		glBindTexture(GL_TEXTURE_2D, skyFBO.getTexID());
		
		Draw2D.drawImage(0, 0, skyFBO.getWidth(), skyFBO.getHeight(), 0, 1, 1, -1, -1);
		
		skyFBOShader.disable();
		
		texToScreenShader.enable();
		texToScreenShader.sendUniformMatrix4("projectionMatrix", projectionMatrix.toFloatBuffer());
		
		//draw far fbo
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, farFBO.getTexID());
		
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, farFBO.getDepthTexID());
		
		texToScreenShader.sendUniform1f("maxDepth", 1.0f);
		Draw2D.drawImage(0, 0, farFBO.getWidth(), farFBO.getHeight(), 0, 1, 1, -1);
		
		//draw near fbo
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, nearFBO.getTexID());
		
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, nearFBO.getDepthTexID());
		
		texToScreenShader.sendUniform1f("maxDepth", getCurrentCamera().getDistanceRatio());
		
		Draw2D.drawImage(0, 0, nearFBO.getWidth(), nearFBO.getHeight(), 0, 1, 1, -1);
		
		//disable textures
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, 0);
		
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, 0);
		
		texToScreenShader.disable();
		
		glDisable(GL_BLEND);
	}
	
	public void render(float timeElapsed) {}
	
	public void exit() {}

	public void keyPressed(int key){}
	
	public final void keyPressedAbs(int key) {
		if(windowManager.keyPressed(key)) {
			return;
		}
		
		currentCamera.keyPressed(key);
		
		nearSphereTree.keyPressed(key);
		farSphereTree.keyPressed(key);
		
		keyPressed(key);
	}
	
	public void keyReleased(int key){}
	
	public final void keyReleasedAbs(int key) {
		if(windowManager.keyReleased(key)) {
			return;
		}
		
		currentCamera.keyReleased(key);
		
		nearSphereTree.keyReleased(key);
		farSphereTree.keyReleased(key);
		
		keyReleased(key);
	}
	
	public void mousePressed(int posX, int posY, int button) {}
	
	public final void mousePressedAbs(int posX, int posY, int button) {
		if(windowManager.mousePressed(posX, posY, button)) {
			return;
		}
		
		currentCamera.mousePressed(posX, posY, button);
		
		nearSphereTree.mousePressed(posX, posY, button);
		farSphereTree.mousePressed(posX, posY, button);
		
		mousePressed(posX, posY, button);
	}
	
	public void mouseReleased(int posX, int posY, int button) {}
	
	public final void mouseReleasedAbs(int posX, int posY, int button) {
		if(windowManager.mouseReleased(posX, posY, button)) {
			return;
		}
		
		currentCamera.mouseReleased(posX, posY, button);
		
		nearSphereTree.mouseReleased(posX, posY, button);
		farSphereTree.mouseReleased(posX, posY, button);
		
		mouseReleased(posX, posY, button);
	}
	
	public void mouseMoved(int posX, int posY, int button) {}
	
	public final void mouseMovedAbs(int posX, int posY, int button) {
		if(windowManager.mouseMoved(posX, posY, button)) {
			return;
		}
		
		currentCamera.mouseMoved(posX, posY, button);
		
		nearSphereTree.mouseMoved(posX, posY, button);
		farSphereTree.mouseMoved(posX, posY, button);
		
		mouseMoved(posX, posY, button);
	}
	
	public void mouseDragged(int posX, int posY, int button) {}
	
	public final void mouseDraggedAbs(int posX, int posY, int button) {
		if(windowManager.mouseDragged(posX, posY, button)) {
			return;
		}

		currentCamera.mouseDragged(posX, posY, button);
		
		nearSphereTree.mouseDragged(posX, posY, button);
		farSphereTree.mouseDragged(posX, posY, button);
		
		mouseDragged(posX, posY, button);
	}
	
	public void mouseWheelMoved(int movement) {}
	
	public final void mouseWheelMovedAbs(int movement) {
		if(windowManager.mouseWheelMoved(movement)) {
			return;
		}

		currentCamera.mouseWheelMoved(movement);
		
		nearSphereTree.mouseWheelMoved(movement);
		farSphereTree.mouseWheelMoved(movement);
		
		mouseWheelMoved(movement);
	}

	public void setOpenGLView(OpenGLView openGLView) {
		this.openGLView = openGLView;
	}
	
	public OpenGLView getOpenGLView() {
		return openGLView;
	}
	
	public void setCurrentScene(String scene) {
		openGLView.setCurrentScene(scene);
	}

	public Object getName() {
		return name;
	}
	
	public void setCurrentCamera(Camera currentCamera) {
		currentCamera.setWidth(windowManager.getWidth());
		currentCamera.setHeight(windowManager.getHeight());
		
		this.currentCamera = currentCamera;
	}
	
	public void setSkyObject(GameObject skyObject) {
		if(this.skyObject != null) {
			this.skyObject.setScene(null);
		}
		
		this.skyObject = skyObject;
		
		if(skyObject != null) {
			skyObject.setScene(this);
		}
	}
	
	public Camera getCurrentCamera() {
		return currentCamera;
	}

	public void setGUIStyle(GUIStyle guiStyle) {
		windowManager.setGuiStyle(guiStyle);
	}
	
	public WindowManager getWindowManager() {
		return windowManager;
	}
	
	public LightManager getLightManager() {
		return lightManager;
	}
	
	public void addPolygons(int number) {
		numPolygons += number;
	}
	
	public void addDrawCall() {
		numDrawCalls++;
	}
	
	public int getNumDrawCalls() {
		return numDrawCalls;
	}
	
	public int getNumPolygons() {
		return numPolygons;
	}
	
	public float getRenderLoopTime() {
		return renderLoopTime;
	}
	
	public GameObject find(String id) {
		GameObject g = nearSphereTree.find(id);
		
		if(g == null) {
			g = farSphereTree.find(id);
		}
		
		return g;
	}
	
	public ArrayList<GameObject> findAll(String id) {
		ArrayList<GameObject> result = new ArrayList<GameObject>();
		
		nearSphereTree.findAll(id, result);
		farSphereTree.findAll(id, result);
		
		return result;
	}
	
	public void setDefaultShader(Shader defaultShader) {
		this.defaultShader = defaultShader;
	}
	
	public Shader getDefaultShader() {
		return defaultShader;
	}
}
